package com.dbflow5.paging;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;

@SuppressWarnings("unused")
public abstract class DataSource<Key, Value> {
    public abstract static class Factory<Key, Value> {
        @NonNull
        public abstract DataSource<Key, Value> create();

        @NonNull
        public <ToValue> DataSource.Factory<Key, ToValue> map(
                @NonNull Function<Value, ToValue> function) {
            return mapByPage(createListFunction(function));
        }

        @NonNull
        public <ToValue> DataSource.Factory<Key, ToValue> mapByPage(
                @NonNull final Function<List<Value>, List<ToValue>> function) {
            return new Factory<Key, ToValue>() {
                @Override
                public DataSource<Key, ToValue> create() {
                    return Factory.this.create().mapByPage(function);
                }
            };
        }
    }

    @NonNull
    static <X, Y> Function<List<X>, List<Y>> createListFunction(
            final @NonNull Function<X, Y> innerFunc) {
        return source -> {
            List<Y> out = new ArrayList<>(source.size());
            for (X x : source) {
                out.add(innerFunc.apply(x));
            }
            return out;
        };
    }

    static <A, B> List<B> convert(Function<List<A>, List<B>> function, List<A> source) {
        List<B> dest = function.apply(source);
        if (dest.size() != source.size()) {
            throw new IllegalStateException("Invalid Function " + function
                    + " changed return size. This is not supported.");
        }
        return dest;
    }

    DataSource() {
    }

    @NonNull
    public abstract <ToValue> DataSource<Key, ToValue> mapByPage(
            @NonNull Function<List<Value>, List<ToValue>> function);

    @NonNull
    public abstract <ToValue> DataSource<Key, ToValue> map(
            @NonNull Function<Value, ToValue> function);

    abstract boolean isContiguous();

    static class LoadCallbackHelper<T> {
        static void validateInitialLoadParams(@NonNull List<?> data, int position, int totalCount) {
            if (position < 0) {
                throw new IllegalArgumentException("Position must be non-negative");
            }
            if (data.size() + position > totalCount) {
                throw new IllegalArgumentException(
                        "List size + position too large, last item in list beyond totalCount.");
            }
            if (data.size() == 0 && totalCount > 0) {
                throw new IllegalArgumentException(
                        "Initial result cannot be empty if items are present in data set.");
            }
        }

        @PageResult.ResultType
        final int mResultType;
        private final DataSource mDataSource;
        final PageResult.Receiver<T> mReceiver;

        // mSignalLock protects mPostExecutor, and mHasSignalled
        private final Object mSignalLock = new Object();
        private Executor mPostExecutor = null;
        private boolean mHasSignalled = false;

        LoadCallbackHelper(@NonNull DataSource dataSource, @PageResult.ResultType int resultType,
                           @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
            mDataSource = dataSource;
            mResultType = resultType;
            mPostExecutor = mainThreadExecutor;
            mReceiver = receiver;
        }

        void setPostExecutor(Executor postExecutor) {
            synchronized (mSignalLock) {
                mPostExecutor = postExecutor;
            }
        }

        boolean dispatchInvalidResultIfInvalid() {
            if (mDataSource.isInvalid()) {
                dispatchResultToReceiver(PageResult.<T>getInvalidResult());
                return true;
            }
            return false;
        }

        void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
            Executor executor;
            synchronized (mSignalLock) {
                if (mHasSignalled) {
                    throw new IllegalStateException(
                            "callback.onResult already called, cannot call again.");
                }
                mHasSignalled = true;
                executor = mPostExecutor;
            }

            if (executor != null) {
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        mReceiver.onPageResult(mResultType, result);
                    }
                });
            } else {
                mReceiver.onPageResult(mResultType, result);
            }
        }
    }

    public interface InvalidatedCallback {
        @AnyThread
        void onInvalidated();
    }

    private final AtomicBoolean mInvalid = new AtomicBoolean(false);

    private final CopyOnWriteArrayList<InvalidatedCallback> mOnInvalidatedCallbacks =
            new CopyOnWriteArrayList<>();

    @AnyThread
    @SuppressWarnings("WeakerAccess")
    public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
        mOnInvalidatedCallbacks.add(onInvalidatedCallback);
    }

    @AnyThread
    @SuppressWarnings("WeakerAccess")
    public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
        mOnInvalidatedCallbacks.remove(onInvalidatedCallback);
    }

    @AnyThread
    public void invalidate() {
        if (mInvalid.compareAndSet(false, true)) {
            for (InvalidatedCallback callback : mOnInvalidatedCallbacks) {
                callback.onInvalidated();
            }
        }
    }

    /**
     * Returns true if the data source is invalid, and can no longer be queried for data.
     *
     * @return True if the data source is invalid, and can no longer return data.
     */
    @WorkerThread
    public boolean isInvalid() {
        return mInvalid.get();
    }
}
